<!--
@license
Copyright 2019 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <link rel="import" href="../../tf-imports/polymer.html" />
    <script src="../../web-component-tester/browser.js"></script>
    <link rel="import" href="../tf-hparams-query-pane.html" />
    <link
      rel="import"
      href="../../tf-hparams-backend/tf-hparams-backend.html"
    />
  </head>
  <body>
    <test-fixture id="tf-hparams-query-pane">
      <template>
        <tf-hparams-query-pane experiment-name="my_experiment">
        </tf-hparams-query-pane>
      </template>
    </test-fixture>
    <script>
      chai.config.truncateThreshold = 0;
      suite('<tf-hparams-query-pane>-tests', function() {
        var element;
        const experimentResponse = {
          description: '',
          timeCreatedSecs: 1528839883.156576,
          hparamInfos: [
            {
              name: 'initial_temperature',
              displayName: 'Initial temperature',
              description: '',
              domainDiscrete: [270.0, 320.0, 370.0],
              type: 'DATA_TYPE_FLOAT64',
            },
            {
              name: 'ambient_temperature',
              displayName: 'Ambient temperature',
              description: '',
              domainInterval: {
                minValue: -5000,
                maxValue: 5000,
              },
              type: 'DATA_TYPE_FLOAT64',
            },
          ],
          metricInfos: [
            {
              name: {
                group: '',
                tag: 'current',
              },
              displayName: 'Current Temp.',
              datasetType: 'DATASET_UNKNOWN',
              description: '',
            },
            {
              name: {
                group: '',
                tag: 'difference_to_ambient',
              },
              displayName: 'Difference To Ambient Temp.',
              datasetType: 'DATASET_UNKNOWN',
              description: '',
            },
          ],
          user: '',
        };
        const sessionGroupsResponse = {
          sessionGroups: [
            {
              name: '0163d6ebd35ff89c6c607a5a0639247c',
              hparams: {
                ambient_temperature: 270.0,
                initial_temperature: 370.0,
                heat_coefficient: 0.001,
              },
              sessions: [
                {
                  name: '40',
                  startTimeSecs: 1528839898.35078,
                  monitorUrl: '',
                  endTimeSecs: 1528839898.676126,
                  metricValues: [
                    {
                      name: {
                        group: '',
                        tag: 'current',
                      },
                      wallTimeSecs: 1528839898.676076,
                      value: 359.4591979980469,
                      trainingStep: 99,
                    },
                    {
                      name: {
                        group: '',
                        tag: 'difference_to_ambient',
                      },
                      wallTimeSecs: 1528839898.676076,
                      value: 89.45919799804688,
                      trainingStep: 99,
                    },
                  ],
                  modelUri: '',
                  status: 'STATUS_SUCCESS',
                },
              ],
            },
          ],
        };

        // Fake the debounce method and make it call its given function
        // synchronously to make the test synchronous (otherwise
        // the _queryServer() method called on element initialization would
        // end-up querying the server after a delay).
        Polymer.Base.debounce = function(name, func, delay) {
          func();
        };

        // Make the window.alert() method a no-op so the element won't block
        // awaiting user input.
        window.alert = function() {};

        setup(function() {
          fakeBackend = sinon.createStubInstance(tf.hparams.Backend);
          fakeBackend.getExperiment.returns(
            Promise.resolve(experimentResponse)
          );
          fakeBackend.listSessionGroups.returns(
            Promise.resolve(sessionGroupsResponse)
          );
          // Grab a fresh copy before each test.
          element = fixture('tf-hparams-query-pane');
          element.set('backend', fakeBackend);
        });

        test('element initialized correctly.', function() {
          return element._getExperimentResolved.then(() => {
            assert.deepEqual(element._hparams, [
              {
                info: experimentResponse.hparamInfos[0],
                filter: {
                  domainDiscrete: [
                    {value: 270, checked: true},
                    {value: 320, checked: true},
                    {value: 370, checked: true},
                  ],
                },
                displayed: true,
              },
              {
                info: experimentResponse.hparamInfos[1],
                filter: {
                  interval: {
                    min: {value: '', invalid: false},
                    max: {value: '', invalid: false},
                  },
                },
                displayed: true,
              },
            ]);
            assert.deepEqual(element._metrics, [
              {
                info: experimentResponse.metricInfos[0],
                filter: {
                  interval: {
                    min: {value: '', invalid: false},
                    max: {value: '', invalid: false},
                  },
                },
                displayed: true,
              },
              {
                info: experimentResponse.metricInfos[1],
                filter: {
                  interval: {
                    min: {value: '', invalid: false},
                    max: {value: '', invalid: false},
                  },
                },
                displayed: true,
              },
            ]);
            assert.deepEqual(
              element.sessionGroups,
              sessionGroupsResponse.sessionGroups
            );
            assert.deepEqual(element.configuration, {
              schema: {
                hparamColumns: experimentResponse.hparamInfos.map((h) => ({
                  hparamInfo: h,
                })),
                metricColumns: experimentResponse.metricInfos.map((m) => ({
                  metricInfo: m,
                })),
              },
              columnsVisibility: [true, true, true, true],
              visibleSchema: {
                hparamInfos: experimentResponse.hparamInfos,
                metricInfos: experimentResponse.metricInfos,
              },
            });
            assert.deepEqual(element._experiment, experimentResponse);
            element._metrics[0].displayed = false;
            element._metrics[1].displayed = false;
            element._hparams[1].displayed = false;
            actualSchema = element._computeSchema();
            assert.deepEqual(actualSchema, {
              hparamColumns: [
                {hparamInfo: experimentResponse.hparamInfos[0]},
                {hparamInfo: experimentResponse.hparamInfos[1]},
              ],
              metricColumns: [
                {metricInfo: experimentResponse.metricInfos[0]},
                {metricInfo: experimentResponse.metricInfos[1]},
              ],
            });
          });
        });

        test('element updates "sessionGroups" in correct order', function() {
          let resolveFirstQuery = null;
          let resolveSecondQuery = null;
          // We set calls #1 and #2 here to return pending promises so
          // we can control their execution order by the fake backend.
          // Note that the first call (#0) is already done when the element
          // initializes.
          fakeBackend.listSessionGroups
            .onCall(1)
            .returns(new Promise((resolve) => (resolveFirstQuery = resolve)));
          fakeBackend.listSessionGroups
            .onCall(2)
            .returns(new Promise((resolve) => (resolveSecondQuery = resolve)));
          return element._getExperimentResolved.then(() => {
            const prevCallCount = fakeBackend.listSessionGroups.callCount;
            const firstQueryDone = element._queryServerNoDebounce();
            const secondQueryDone = element._queryServerNoDebounce();

            assert.equal(
              prevCallCount + 2,
              fakeBackend.listSessionGroups.callCount
            );

            // Respond first to the second request.
            resolveSecondQuery({sessionGroups: []});
            // And then to the first request.
            resolveFirstQuery(sessionGroupsResponse);
            // Wait until the element processes both queries and then test.
            return Promise.all([firstQueryDone, secondQueryDone]).then(() => {
              assert.deepEqual(element._hparams, [
                {
                  info: experimentResponse.hparamInfos[0],
                  filter: {
                    domainDiscrete: [
                      {value: 270, checked: true},
                      {value: 320, checked: true},
                      {value: 370, checked: true},
                    ],
                  },
                  displayed: true,
                },
                {
                  info: experimentResponse.hparamInfos[1],
                  filter: {
                    interval: {
                      min: {value: '', invalid: false},
                      max: {value: '', invalid: false},
                    },
                  },
                  displayed: true,
                },
              ]);
              assert.deepEqual(element.sessionGroups, []);
            });
          });
        });

        test(
          'element updates "sessionGroups" after a successful request' +
            ' following a failed request',
          function() {
            let rejectFirstQuery = null;
            let resolveSecondQuery = null;
            // We set calls #1 and #2 here to return pending promises so
            // we can control their execution order by the fake backend.
            // Note that the first call (#0) is already done when the element
            // initializes.
            fakeBackend.listSessionGroups
              .onCall(1)
              .returns(
                new Promise((resolve, reject) => (rejectFirstQuery = reject))
              );
            fakeBackend.listSessionGroups
              .onCall(2)
              .returns(
                new Promise((resolve) => (resolveSecondQuery = resolve))
              );
            return element._getExperimentResolved.then(() => {
              const prevCallCount = fakeBackend.listSessionGroups.callCount;
              const firstQueryDone = element._queryServerNoDebounce().then(
                () => {
                  assert.fail('Expected first query to fail!');
                },
                () => {
                  /* Ok. First query failed.  */
                }
              );
              const secondQueryDone = element._queryServerNoDebounce();

              assert.equal(
                prevCallCount + 2,
                fakeBackend.listSessionGroups.callCount
              );

              // Respond with an error to the first request.
              rejectFirstQuery();
              // And then with an OK status to the second request.
              resolveSecondQuery(sessionGroupsResponse);
              // Wait until the element processes both queries and then test.
              return Promise.all([firstQueryDone, secondQueryDone]).then(() => {
                assert.deepEqual(element._hparams, [
                  {
                    info: experimentResponse.hparamInfos[0],
                    filter: {
                      domainDiscrete: [
                        {value: 270, checked: true},
                        {value: 320, checked: true},
                        {value: 370, checked: true},
                      ],
                    },
                    displayed: true,
                  },
                  {
                    info: experimentResponse.hparamInfos[1],
                    filter: {
                      interval: {
                        min: {value: '', invalid: false},
                        max: {value: '', invalid: false},
                      },
                    },
                    displayed: true,
                  },
                ]);
                assert.deepEqual(
                  element.sessionGroups,
                  sessionGroupsResponse.sessionGroups
                );
              });
            });
          }
        );

        test('element sends correct ListSessionGroupsRequest', function() {
          return element._getExperimentResolved.then(() => {
            element._hparams[0].filter = {
              domainDiscrete: [
                {value: 270, checked: true},
                {value: 320, checked: false},
                {value: 370, checked: false},
              ],
            };
            element._hparams[1].filter = {
              interval: {
                min: {value: '300', invalid: false},
                max: {value: '423', invalid: false},
              },
            };
            element._metrics[0].filter = {
              interval: {
                min: {value: '123', invalid: false},
                max: {value: '456', invalid: false},
              },
            };
            element._metrics[1].filter = {
              interval: {
                min: {value: '', invalid: false},
                max: {value: '', invalid: false},
              },
            };
            // STATUS_UNKNONW
            element._statuses[0].allowed = false;
            // STATUS_SUCCESS
            element._statuses[1].allowed = false;
            // STATUS_FAILURE
            element._statuses[2].allowed = true;
            // STATUS_RUNNING
            element._statuses[3].allowed = true;
            element._sortByIndex = 2; // Sort by the first metric.
            element._sortDirection = 1; // In descending order.
            assert.deepEqual(element._buildListSessionGroupsRequest(), {
              experimentName: 'my_experiment',
              allowedStatuses: ['STATUS_FAILURE', 'STATUS_RUNNING'],
              colParams: [
                {
                  hparam: experimentResponse.hparamInfos[0].name,
                  filterDiscrete: [270],
                },
                {
                  hparam: experimentResponse.hparamInfos[1].name,
                  filterInterval: {minValue: 300, maxValue: 423},
                },
                {
                  metric: {group: '', tag: 'current'},
                  filterInterval: {minValue: 123, maxValue: 456},
                  order: 'ORDER_DESC',
                },
                {
                  metric: {group: '', tag: 'difference_to_ambient'},
                  filterInterval: {minValue: '-Infinity', maxValue: 'Infinity'},
                },
              ],
              startIndex: 0,
              sliceSize: 100,
            });
          });
        });
      });
    </script>
  </body>
</html>
